package org.dromara.common.excel.utils;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.resource.ClassPathResource;
import cn.hutool.core.util.IdUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.fill.FillConfig;
import com.alibaba.excel.write.metadata.fill.FillWrapper;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.file.FileUtils;
import org.dromara.common.excel.convert.ExcelBigNumberConvert;
import org.dromara.common.excel.core.*;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Collection;
import java.util.List;
import java.util.Map;


/**
 * @Author 刘武贵
 * @Date 2024/8/15 22:00
 * @Description 功能描述：Excel相关处理
 */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ExcelUtil {

    /**
     * 同步导入(适用于小数据量)
     *
     * @param is 输入流
     * @param clazz 指定要转换的数据类型
     * @param <T> 泛型参数，表示要导入的数据类型
     * @return 转换后集合
     */
    public static <T> List<T> importExcel(InputStream is, Class<T> clazz) {
        // 使用EasyExcel读取输入流中的Excel数据，指定数据头部类型为clazz，关闭自动关闭流功能，读取第一个工作表，并同步执行读取操作
        return EasyExcel.read(is).head(clazz).autoCloseStream(false).sheet().doReadSync();
    }


    /**
     * 使用校验监听器 异步导入 同步返回
     *
     * @param is         输入流
     * @param clazz      对象类型
     * @param isValidate 是否 Validator 检验 默认为是
     * @return 转换后集合
     */
    public static <T> ExcelResult<T> importExcel(InputStream is, Class<T> clazz, boolean isValidate) {
        // 创建一个DefaultExcelListener对象，用于监听读取过程中的事件，并设置是否进行数据校验
        DefaultExcelListener<T> listener = new DefaultExcelListener<>(isValidate);

        // 使用EasyExcel读取输入流中的Excel数据，指定读取的类型、监听器，并指定要读取的工作表
        // .doRead()方法执行实际的读取操作
        return listener.getExcelResult();
    }

    /**
     * 使用自定义监听器 异步导入 自定义返回
     *
     * @param is       输入流，用于读取Excel文件的内容
     * @param clazz    对象类型，数据对象的类，用于定义数据的类型
     * @param listener 自定义监听器，用于处理读取到的数据并存储
     * @param <T> 泛型参数，表示数据对象的类型
     * @return 转换后集合。返回包含读取结果的ExcelResult对象
     */
    public static <T> ExcelResult<T> importExcel(InputStream is, Class<T> clazz, ExcelListener<T> listener) {
        // 使用EasyExcel读取Excel文件，指定读取的类和监听器
        EasyExcel.read(is, clazz, listener).sheet().doRead();
        // 返回监听器中存储的读取结果
        return listener.getExcelResult();
    }

    /**
     * 导出excel
     *
     * @param list      导出数据集合
     * @param sheetName 工作表的名称
     * @param clazz     实体类
     * @param response  响应体
     */
    public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, HttpServletResponse response) {
        try {
            resetResponse(sheetName, response);
            ServletOutputStream os = response.getOutputStream();
            exportExcel(list, sheetName, clazz, false, os, null);
        } catch (IOException e) {
            throw new RuntimeException("导出Excel异常");
        }
    }

    /**
     * 导出excel
     *
     * @param list      导出数据集合
     * @param sheetName 工作表的名称
     * @param clazz     实体类
     * @param response  响应体
     * @param options   级联下拉选
     */
    public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, HttpServletResponse response, List<DropDownOptions> options) {
        try {
            resetResponse(sheetName, response);
            ServletOutputStream os = response.getOutputStream();
            exportExcel(list, sheetName, clazz, false, os, options);
        } catch (IOException e) {
            throw new RuntimeException("导出Excel异常");
        }
    }

    /**
     * 导出excel
     *
     * @param list      导出数据集合
     * @param sheetName 工作表的名称
     * @param clazz     实体类
     * @param merge     是否合并单元格
     * @param response  响应体
     */
    public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge, HttpServletResponse response) {
        try {
            resetResponse(sheetName, response);
            ServletOutputStream os = response.getOutputStream();
            exportExcel(list, sheetName, clazz, merge, os, null);
        } catch (IOException e) {
            throw new RuntimeException("导出Excel异常");
        }
    }

    /**
     * 导出excel
     *
     * @param list      导出数据集合
     * @param sheetName 工作表的名称
     * @param clazz     实体类
     * @param merge     是否合并单元格
     * @param response  响应体
     * @param options   级联下拉选
     */
    public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge, HttpServletResponse response, List<DropDownOptions> options) {
        try {
            resetResponse(sheetName, response);
            ServletOutputStream os = response.getOutputStream();
            exportExcel(list, sheetName, clazz, merge, os, options);
        } catch (IOException e) {
            throw new RuntimeException("导出Excel异常");
        }
    }

    /**
     * 导出excel
     *
     * @param list      导出数据集合
     * @param sheetName 工作表的名称
     * @param clazz     实体类
     * @param os        输出流
     */
    public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, OutputStream os) {
        exportExcel(list, sheetName, clazz, false, os, null);
    }

    /**
     * 导出excel
     *
     * @param list      导出数据集合
     * @param sheetName 工作表的名称
     * @param clazz     实体类
     * @param os        输出流
     * @param options   级联下拉选内容
     */
    public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, OutputStream os, List<DropDownOptions> options) {
        exportExcel(list, sheetName, clazz, false, os, options);
    }

    /**
     * 导出excel
     *
     * @param list      导出数据集合
     * @param sheetName 工作表的名称
     * @param clazz     实体类
     * @param merge     是否合并单元格
     * @param os        输出流
     * @param options 下拉菜单选项列表
     */
    public static <T> void exportExcel(List<T> list, String sheetName, Class<T> clazz, boolean merge,
                                       OutputStream os, List<DropDownOptions> options) {
        ExcelWriterSheetBuilder builder = EasyExcel.write(os, clazz)
            // 禁止自动关闭流，以便后续操作
            .autoCloseStream(false)
            // 自动适配
            .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
            // 大数值自动转换 防止失真
            .registerConverter(new ExcelBigNumberConvert())
            .sheet(sheetName);
        if (merge) {
            // 合并处理器
            builder.registerWriteHandler(new CellMergeStrategy(list, true));
        }
        // 添加下拉框操作
        builder.registerWriteHandler(new ExcelDownHandler(options));
        // 执行写入操作
        builder.doWrite(list);
    }

    /**
     * 单表多数据模板导出 模板格式为 {.属性}
     *
     * @param filename     文件名
     * @param templatePath 模板路径 resource 目录下的路径包括模板文件名
     *                     例如: excel/temp.xlsx
     *                     重点: 模板文件必须放置到启动类对应的 resource 目录下
     * @param data         模板需要的数据
     * @param response     响应体
     */
    public static void exportTemplate(List<Object> data, String filename, String templatePath, HttpServletResponse response) {
        try {
            resetResponse(filename, response);
            ServletOutputStream os = response.getOutputStream();
            exportTemplate(data, templatePath, os);
        } catch (IOException e) {
            throw new RuntimeException("导出Excel异常");
        }
    }

    /**
     * 单表多数据模板导出 模板格式为 {.属性}
     * 导出模板方法，通过数据和模板路径生成Excel文件
     *
     * @param templatePath 模板路径 resource 目录下的路径包括模板文件名
     *                     例如: excel/temp.xlsx
     *                     重点: 模板文件必须放置到启动类对应的 resource 目录下
     * @param data         模板需要的数据，表示要导出到Excel的数据列表，每个对象代表一行数据
     * @param os           输出流，用于输出生成的Excel文件
     * @throws IllegalArgumentException 如果数据列表为空，抛出此异常
     */
    public static void exportTemplate(List<Object> data, String templatePath, OutputStream os) {
        // 加载模板资源
        ClassPathResource templateResource = new ClassPathResource(templatePath);
        ExcelWriter excelWriter = EasyExcel.write(os)
            // 设置模板流
            .withTemplate(templateResource.getStream())
            // 禁止自动关闭流，以便后续操作
            .autoCloseStream(false)
            // 大数值自动转换 防止失真
            .registerConverter(new ExcelBigNumberConvert())
            .build();
        WriteSheet writeSheet = EasyExcel.writerSheet().build();
        if (CollUtil.isEmpty(data)) {
            throw new IllegalArgumentException("数据为空");
        }
        // 单表多数据导出 模板格式为 {.属性}
        // 循环数据列表，填充每个数据到Excel模板中
        // 使用模板格式{.属性}进行数据填充
        for (Object d : data) {
            excelWriter.fill(d, writeSheet);
        }
        excelWriter.finish();
    }

    /**
     * 多表多数据模板导出 模板格式为 {key.属性}
     *
     * @param filename     文件名
     * @param templatePath 模板路径 resource 目录下的路径包括模板文件名
     *                     例如: excel/temp.xlsx
     *                     重点: 模板文件必须放置到启动类对应的 resource 目录下
     * @param data         模板需要的数据
     * @param response     响应体
     */
    public static void exportTemplateMultiList(Map<String, Object> data, String filename, String templatePath, HttpServletResponse response) {
        try {
            resetResponse(filename, response);
            ServletOutputStream os = response.getOutputStream();
            // 使用模板导出多页签Excel
            exportTemplateMultiList(data, templatePath, os);
        } catch (IOException e) {
            throw new RuntimeException("导出Excel异常");
        }
    }

    /**
     * 多sheet模板导出 模板格式为 {key.属性}
     *
     * @param filename     文件名
     * @param templatePath 模板路径 resource 目录下的路径包括模板文件名
     *                     例如: excel/temp.xlsx
     *                     重点: 模板文件必须放置到启动类对应的 resource 目录下
     * @param data         模板需要的数据
     * @param response     响应体
     */
    public static void exportTemplateMultiSheet(List<Map<String, Object>> data, String filename, String templatePath, HttpServletResponse response) {
        try {
            resetResponse(filename, response);
            ServletOutputStream os = response.getOutputStream();
            // 使用模板导出多页签Excel
            exportTemplateMultiSheet(data, templatePath, os);
        } catch (IOException e) {
            throw new RuntimeException("导出Excel异常");
        }
    }

    /**
     * 多表多数据模板导出 模板格式为 {key.属性}
     * 导出多表模板Excel
     *
     * @param templatePath 模板路径 resource 目录下的路径包括模板文件名
     *                     例如: excel/temp.xlsx
     *                     重点: 模板文件必须放置到启动类对应的 resource 目录下
     * @param data         模板需要的数据，Map结构，键为表格名称，值为表格数据（单表为Map，多表为Map列表）
     * @param os           输出流
     * @throws IOException 如果读取模板或输出过程中发生错误
     * @throws IllegalArgumentException 如果输入的数据为空
     */
    public static void exportTemplateMultiList(Map<String, Object> data, String templatePath, OutputStream os) {
        // 加载模板资源
        ClassPathResource templateResource = new ClassPathResource(templatePath);
        // 初始化Excel写对象，使用模板流，防止大数值自动转换导致数据失真
        ExcelWriter excelWriter = EasyExcel.write(os)
            // 设置模板流
            .withTemplate(templateResource.getStream())
            // 禁止自动关闭流，以便后续操作
            .autoCloseStream(false)
            // 大数值自动转换 防止失真
            .registerConverter(new ExcelBigNumberConvert())
            .build();
        // 构建写入表单
        WriteSheet writeSheet = EasyExcel.writerSheet().build();
        if (CollUtil.isEmpty(data)) {
            throw new IllegalArgumentException("数据为空");
        }
        // 遍历数据，逐表写入
        for (Map.Entry<String, Object> map : data.entrySet()) {
            // 设置列表后续还有数据
            FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
            // 判断数据类型，单表或列表
            if (map.getValue() instanceof Collection) {
                // 多表导出必须使用 FillWrapper
                excelWriter.fill(new FillWrapper(map.getKey(), (Collection<?>) map.getValue()), fillConfig, writeSheet);
            } else {
                // 对于单条数据，直接写入
                excelWriter.fill(map.getValue(), writeSheet);
            }
        }
        excelWriter.finish();
    }

    /**
     * 多sheet模板导出 模板格式为 {key.属性}
     * 使用模板导出多页签Excel
     *
     * @param templatePath 模板路径 resource 目录下的路径包括模板文件名
     *                     例如: excel/temp.xlsx
     *                     重点: 模板文件必须放置到启动类对应的 resource 目录下
     * @param data         模板需要的数据 要导出的数据，每个元素对应一个页签的数据，类型为Map，键为页签名，值为页签数据
     * @param os           输出流，用于将生成的Excel文件输出到指定位置
     * @throws IOException 如果读取模板或输出Excel时发生I/O错误
     */
    public static void exportTemplateMultiSheet(List<Map<String, Object>> data, String templatePath, OutputStream os) {
        // 根据模板路径加载模板资源
        ClassPathResource templateResource = new ClassPathResource(templatePath);
        // 初始化ExcelWriter，用于写入Excel数据
        ExcelWriter excelWriter = EasyExcel.write(os)
            // 设置模板流
            .withTemplate(templateResource.getStream())
            // 禁止自动关闭流，以便后续操作
            .autoCloseStream(false)
            // 大数值自动转换 防止失真
            // 注册大数值转换器，防止大数值在Excel中失真
            .registerConverter(new ExcelBigNumberConvert())
            .build();
        if (CollUtil.isEmpty(data)) {
            throw new IllegalArgumentException("数据为空");
        }
        // 遍历每个页签的数据
        for (int i = 0; i < data.size(); i++) {
            // 创建页签写入器
            WriteSheet writeSheet = EasyExcel.writerSheet(i).build();
            // 遍历当前页签的数据项
            for (Map.Entry<String, Object> map : data.get(i).entrySet()) {
                // 设置列表后续还有数据
                // 准备填充值配置，用于控制填充行为
                FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
                // 判断数据项的值是否为集合类型，以决定是否使用FillWrapper
                if (map.getValue() instanceof Collection) {
                    // 多表导出必须使用 FillWrapper
                    // 使用FillWrapper包装集合数据，以便正确填充到Excel
                    excelWriter.fill(new FillWrapper(map.getKey(), (Collection<?>) map.getValue()), fillConfig, writeSheet);
                } else {
                    // 直接填充非集合类型的值到Excel
                    excelWriter.fill(map.getValue(), writeSheet);
                }
            }
        }
        // 完成Excel写入操作
        excelWriter.finish();
    }

    /**
     * 重置响应体
     * 重置响应对象以支持文件下载
     *
     * @param sheetName 工作表名称，用于设置下载文件的名称
     * @param response HttpServletResponse对象，用于设置响应头和内容类型
     * @throws UnsupportedEncodingException 如果字符编码不被支持
     */
    private static void resetResponse(String sheetName, HttpServletResponse response) throws UnsupportedEncodingException {
        // 将工作表名称转换为适合文件名的编码格式
        String filename = encodingFilename(sheetName);
        // 设置响应头信息，使浏览器识别为文件下载
        FileUtils.setAttachmentResponseHeader(response, filename);
        // 设置响应内容类型为Excel文件格式，确保浏览器正确处理
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");
    }

    /**
     * 解析导出值 0=男,1=女,2=未知
     *
     * @param propertyValue 参数值 待转换的属性值
     * @param converterExp  翻译注解 转换表达式，形如"source1=target1,source2=target2"
     * @param separator     分隔符，用于区分属性值中的不同值
     * @return 解析后值 转换后的字符串如果转换失败则返回原始值
     */
    public static String convertByExp(String propertyValue, String converterExp, String separator) {
        // 构建最终的转换后的字符串
        StringBuilder propertyString = new StringBuilder();
        // 根据分隔符分割转换表达式
        String[] convertSource = converterExp.split(StringUtils.SEPARATOR);
        // 遍历转换表达式的每一项
        for (String item : convertSource) {
            // 将每一项再次分割为源值和目标值
            String[] itemArray = item.split("=");
            // 检查属性值是否包含分隔符，即是否有多个值需要转换
            if (StringUtils.containsAny(propertyValue, separator)) {
                // 分割属性值为多个单独的值
                for (String value : propertyValue.split(separator)) {
                    // 检查当前值是否与转换项的源值匹配
                    if (itemArray[0].equals(value)) {
                        // 如果匹配，则将转换后的值追加到结果字符串中，并加上分隔符
                        propertyString.append(itemArray[1] + separator);
                        // 找到匹配后即跳出循环，不再继续尝试其他转换项
                        break;
                    }
                }
            } else {
                // 如果属性值不包含分隔符，则直接检查是否与转换项的源值匹配
                if (itemArray[0].equals(propertyValue)) {
                    // 如果匹配，则直接返回转换后的值
                    return itemArray[1];
                }
            }
        }
        // 移除结果字符串最后的分隔符并返回
        return StringUtils.stripEnd(propertyString.toString(), separator);
    }

    /**
     * 反向解析值 男=0,女=1,未知=2
     * 根据转换表达式对属性值进行反转
     *
     * @param propertyValue 参数值 待转换的属性值
     * @param converterExp  翻译注解 转换表达式，形如"a=1,b=2,c=3"
     * @param separator     分隔符 属性值中的分隔符，如","
     * @return 解析后值 转换后的属性值
     */
    public static String reverseByExp(String propertyValue, String converterExp, String separator) {
        // 构建最终的属性字符串
        StringBuilder propertyString = new StringBuilder();
        // 根据分隔符分割转换表达式
        String[] convertSource = converterExp.split(StringUtils.SEPARATOR);
        // 遍历转换表达式的每一项
        for (String item : convertSource) {
            // 分割每一项为键值对
            String[] itemArray = item.split("=");
            // 检查属性值中是否包含分隔符
            if (StringUtils.containsAny(propertyValue, separator)) {
                // 根据分隔符分割属性值
                for (String value : propertyValue.split(separator)) {
                    // 检查当前值是否与转换表达式中的值匹配
                    if (itemArray[1].equals(value)) {
                        // 如果匹配，将键追加到结果字符串中，并加上分隔符，然后跳出循环
                        propertyString.append(itemArray[0] + separator);
                        break;
                    }
                }
            } else {
                // 如果属性值不包含分隔符，则直接检查是否与转换表达式中的值匹配
                if (itemArray[1].equals(propertyValue)) {
                    // 如果匹配，返回对应的键
                    return itemArray[0];
                }
            }
        }
        // 移除结果字符串最后的分隔符并返回
        return StringUtils.stripEnd(propertyString.toString(), separator);
    }

    /**
     * 编码文件名
     * 对给定的文件名进行编码
     * 生成一个与原文件名相关的唯一文件名，用于避免文件覆盖和重复
     *
     * @param filename 原始文件名
     * @return 编码后的唯一文件名，包含原始文件名和".xlsx"后缀
     */
    public static String encodingFilename(String filename) {
        // 拼接唯一标识符、原始文件名和文件格式后缀，形成新的文件名
        return IdUtil.fastSimpleUUID() + "_" + filename + ".xlsx";
    }

}
